package com.tanhd.flutter_ble_plugin;

import android.Manifest;
import androidx.annotation.NonNull;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.app.Application;

import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import android.content.pm.PackageManager;
import androidx.core.content.ContextCompat;
import android.os.Build;
import android.os.Handler;
import android.os.ParcelUuid;
import android.util.Log;
import android.content.Intent;
import android.util.SparseArray;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener;
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Optional;

/** FlutterBlePlugin */
public class FlutterBlePlugin implements
        FlutterPlugin,
        MethodCallHandler,
        RequestPermissionsResultListener,
        ActivityResultListener,
        ActivityAware {
    private static final String TAG = "[FBP-Android]";
    private LogLevel logLevel = LogLevel.DEBUG;

    private MethodChannel channel;
    private HoldBluetooth mHoldBluetooth;

    private Context mContext;
    private FlutterPluginBinding pluginBinding;
    private final Map<Integer, OperationOnPermission> operationsOnPermission = new HashMap<>();
    private int lastEventId = 1452;
    private ActivityPluginBinding activityBinding;
    private boolean mIsScanning = false;
    // 事件通道
    private EventChannel.EventSink eventChannel;

    private ArrayList<DeviceModule> mDeviceList = new ArrayList<>();


    private interface OperationOnPermission {
        void op(boolean granted, String permission);
    }

    final HoldBluetooth.OnReadDataListener readDataListener = new HoldBluetooth.OnReadDataListener() {
        @Override
        public void readData(String mac, byte[] data) {
            ArrayList<Byte> bytes = new ArrayList<>();
            for (byte b : data) {
                bytes.add(b);
            }
            Map<String, Object> result = new HashMap<>();
            result.put("type", "onReceiveData");
            result.put("mac", mac);
            result.put("data", bytes);
            eventChannel.success(result);
        }

        @Override
        public void reading(boolean isStart) {

        }

        @Override
        public void connectSucceed(String mac) {
            Map<String, Object> result = new HashMap<>();
            result.put("type", "onConnectionStateChange");
            result.put("mac", mac);
            result.put("isConnected", true);
            eventChannel.success(result);
        }

        @Override
        public void errorDisconnect(DeviceModule deviceModule) {
            Map<String, Object> result = new HashMap<>();
            result.put("type", "onConnectionStateChange");
            result.put("mac", deviceModule.getMac());
            result.put("isConnected", false);
            eventChannel.success(result);
        }

        @Override
        public void readNumber(int number) {

        }

        @Override
        public void readLog(String className, String data, String lv) {

        }

        @Override
        public void readVelocity(int velocity) {

        }
    };

    final HoldBluetooth.UpdateList updateList = new HoldBluetooth.UpdateList() {
        @Override
        public void update(boolean isStart, DeviceModule deviceModule) {
            if (deviceModule == null)
                return;

            mDeviceList.add(deviceModule);
            log(LogLevel.DEBUG, "new device");
            Map<String, Object> result = new HashMap<>();
            result.put("type", "onScanResult");
            result.put("mac", deviceModule.getMac());
            result.put("name", deviceModule.getName());
            eventChannel.success(result);
        }

        @Override
        public void updateMessyCode(boolean isStart, DeviceModule deviceModule) {
        }
    };

    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
        pluginBinding = flutterPluginBinding;
        this.mContext = (Application) pluginBinding.getApplicationContext();

        channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "flutter_ble_plugin/methods");
        channel.setMethodCallHandler(this);

        new EventChannel(flutterPluginBinding.getBinaryMessenger(), "flutter_ble_plugin/events")
                .setStreamHandler(new EventChannel.StreamHandler() {
            @Override
            public void onListen(Object arguments, EventChannel.EventSink events) {
                eventChannel = events;
            }

            @Override
            public void onCancel(Object arguments) {
                eventChannel = null;
            }
        });

    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
        channel.setMethodCallHandler(null);
    }

    //////////////////////////////////////////////////////////////////////////////////////
    // ██████ ███████ ██████ ███ ███ ██ ███████ ███████ ██ ██████ ███ ██
    // ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ████ ██
    // ██████ █████ ██████ ██ ████ ██ ██ ███████ ███████ ██ ██ ██ ██ ██ ██
    // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
    // ██ ███████ ██ ██ ██ ██ ██ ███████ ███████ ██ ██████ ██ ████

    @Override
    public boolean onRequestPermissionsResult(int requestCode,
                                              String[] permissions,
                                              int[] grantResults) {
        OperationOnPermission operation = operationsOnPermission.get(requestCode);

        if (operation != null && grantResults.length > 0) {
            operation.op(grantResults[0] == PackageManager.PERMISSION_GRANTED, permissions[0]);
            return true;
        } else {
            return false;
        }
    }

    private void ensurePermissions(List<String> permissions, OperationOnPermission operation) {
        // only request permission we don't already have
        List<String> permissionsNeeded = new ArrayList<>();
        for (String permission : permissions) {
            if (permission != null
                    && ContextCompat.checkSelfPermission(mContext, permission) != PackageManager.PERMISSION_GRANTED) {
                permissionsNeeded.add(permission);
            }
        }

        // no work to do?
        if (permissionsNeeded.isEmpty()) {
            operation.op(true, null);
            return;
        }

        askPermission(permissionsNeeded, operation);
    }

    private void askPermission(List<String> permissionsNeeded, OperationOnPermission operation) {
        // finished asking for permission? call callback
        if (permissionsNeeded.isEmpty()) {
            operation.op(true, null);
            return;
        }

        String nextPermission = permissionsNeeded.remove(0);

        operationsOnPermission.put(lastEventId, (granted, perm) -> {
            operationsOnPermission.remove(lastEventId);
            if (!granted) {
                operation.op(false, perm);
                return;
            }
            // recursively ask for next permission
            askPermission(permissionsNeeded, operation);
        });

        ActivityCompat.requestPermissions(
                activityBinding.getActivity(),
                new String[] { nextPermission },
                lastEventId);

        lastEventId++;
    }

    //////////////////////////////////////////
    // ██ ██ ████████ ██ ██ ███████
    // ██ ██ ██ ██ ██ ██
    // ██ ██ ██ ██ ██ ███████
    // ██ ██ ██ ██ ██ ██
    // ██████ ██ ██ ███████ ███████

    private void log(LogLevel level, String message) {
        if (level.ordinal() > logLevel.ordinal()) {
            return;
        }
        switch (level) {
            case DEBUG:
                Log.d(TAG, "[FBP] " + message);
                break;
            case WARNING:
                Log.w(TAG, "[FBP] " + message);
                break;
            case ERROR:
                Log.e(TAG, "[FBP] " + message);
                break;
            default:
                Log.d(TAG, "[FBP] " + message);
                break;
        }
    }

    enum LogLevel {
        NONE, // 0
        ERROR, // 1
        WARNING, // 2
        INFO, // 3
        DEBUG, // 4
        VERBOSE // 5
    }

    //////////////////////////////////////////////////////////////////////
    // █████ ██████ ████████ ██ ██ ██ ██ ████████ ██ ██
    // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
    // ███████ ██ ██ ██ ██ ██ ██ ██ ████
    // ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
    // ██ ██ ██████ ██ ██ ████ ██ ██ ██
    //
    // ██████ ███████ ███████ ██ ██ ██ ████████
    // ██ ██ ██ ██ ██ ██ ██ ██
    // ██████ █████ ███████ ██ ██ ██ ██
    // ██ ██ ██ ██ ██ ██ ██ ██
    // ██ ██ ███████ ███████ ██████ ███████ ██

    @Override
    public boolean onActivityResult(int requestCode, int resultCode, Intent data) {

        return false; // did not handle anything
    }

    @Override
    public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
        log(LogLevel.DEBUG, "onAttachedToActivity");
        activityBinding = binding;
        activityBinding.addRequestPermissionsResultListener(this);
        activityBinding.addActivityResultListener(this);
    }

    @Override
    public void onDetachedFromActivityForConfigChanges() {
        log(LogLevel.DEBUG, "onDetachedFromActivityForConfigChanges");
        onDetachedFromActivity();
    }

    @Override
    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
        log(LogLevel.DEBUG, "onReattachedToActivityForConfigChanges");
        onAttachedToActivity(binding);
    }

    @Override
    public void onDetachedFromActivity() {
        log(LogLevel.DEBUG, "onDetachedFromActivity");
        activityBinding.removeRequestPermissionsResultListener(this);
        activityBinding = null;
    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
        if (mHoldBluetooth == null) {
            if (activityBinding != null) {
                mHoldBluetooth = HoldBluetooth.getInstance();
                mHoldBluetooth.setDevelopmentMode(mContext);
                mHoldBluetooth.initHoldBluetooth(activityBinding.getActivity(), updateList);
                mHoldBluetooth.setOnReadListener(readDataListener);
            }
        }

        switch (call.method) {
            case "getPlatformVersion":
                result.success("Android " + Build.VERSION.RELEASE);
                break;
            case "startScan": {
                ArrayList<String> permissions = new ArrayList<>();

                if (Build.VERSION.SDK_INT >= 31) { // Android 12 (October 2021)
                    permissions.add(Manifest.permission.BLUETOOTH_SCAN);
                    permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
                    // it is unclear why this is needed, but some phones throw a
                    // SecurityException AdapterService getRemoteName, without it
                    permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
                }

                if (Build.VERSION.SDK_INT <= 30) { // Android 11 (September 2020)
                    permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
                }

                ensurePermissions(permissions, (granted, perm) -> {
                    if (granted == false) {
                        result.error("startScan",
                                String.format("FlutterBluePlus requires %s permission", perm), null);
                        return;
                    }

                    mDeviceList.clear();
                    mHoldBluetooth.scan(true);
                    mIsScanning = true;
                    result.success(true);
                });
            }
            break;
            case "stopScan": {
                if (mIsScanning) {
                    mHoldBluetooth.stopScan();
                    mIsScanning = false;
                }
                result.success(true);
                break;
            }
            case "isScanning": {
                result.success(mIsScanning);
                break;
            }
            case "connect": {
                String deviceId = (String) call.arguments();
                if (deviceId != null) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        Optional<DeviceModule> deviceModule = mDeviceList.stream()
                                .filter(device -> device.getMac().equals(deviceId))
                                .findFirst();
                        if (deviceModule.isPresent()) {
                            mHoldBluetooth.connect(deviceModule.get());
                            result.success(true);
                            return;
                        }
                    }
                }
                result.success(false);
                break;
            }
            case "disconnect": {
                HashMap<String, Object> data = call.arguments();
                String deviceId = (String) data.get("mac");
                if (deviceId != null) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        Optional<DeviceModule> deviceModule = mDeviceList.stream()
                                .filter(device -> device.getMac().equals(deviceId))
                                .findFirst();
                        if (deviceModule.isPresent()) {
                            mHoldBluetooth.disconnect(deviceModule.get());
                            result.success(true);
                            return;
                        }
                    }
                }
                result.success(false);
                break;
            }
            case "sendData": {
                HashMap<String, Object> data = call.arguments();
                String deviceId = (String) data.get("mac");
                if (deviceId != null) {
                    Optional<DeviceModule> deviceModule = null;
                    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
                        deviceModule = mDeviceList.stream()
                                .filter(device -> device.getMac().equals(deviceId))
                                .findFirst();
                        if (deviceModule.isPresent()) {
                            byte[] bytes = (byte[]) data.get("data");
                            mHoldBluetooth.sendData(deviceModule.get(), bytes);
                            result.success(true);
                            return;
                        }
                    }
                }
                result.success(false);
                break;
            }
            default:
                result.notImplemented();
                break;
        }

    }

}
